// Complete Reference — 2024

Android
Testing
Mastery

Every test type, every tool, every pattern. A thorough guide to building rock-solid Android applications through comprehensive testing strategies.

8+ Test Types
20+ Tools Covered
100% Coverage Guide
JUnit5 Espresso Mockito Robolectric UI Automator Hilt Testing Compose Testing Firebase Test Lab MockK Turbine JUnit5 Espresso Mockito Robolectric UI Automator Hilt Testing Compose Testing Firebase Test Lab MockK Turbine
// 01 — Foundation

The Testing
Pyramid

A strategic model that guides how to balance different types of tests for maximum confidence and minimum cost.

E2E Tests  ·  Slow, High Confidence
Integration Tests  ·  Medium
Unit Tests  ·  Fast, Isolated
Unit Tests — 70%
Test individual functions, classes, and logic in isolation. Run in milliseconds. Form the base of your test suite. Target ViewModels, Repositories, UseCases, and pure business logic.
Integration Tests — 20%
Verify that multiple components work together correctly. Test database + repository, API + data layer, and component interactions. Run on device or emulator.
E2E / UI Tests — 10%
Simulate real user journeys across the full app. Slowest and most brittle but highest confidence. Use sparingly for critical user flows like login, checkout, or onboarding.
// 02 — Test Types

Every Testing
Category

From unit to security — a complete breakdown of all Android testing disciplines.

01
Unit Testing
Unit Tests
Tests that verify a single unit of code (function, class, method) in complete isolation. Dependencies are mocked or stubbed. These run on the JVM without a device.
JUnit4 / JUnit5 Mockito MockK Truth Turbine Kotest
What to test
  • ViewModel state & logic
  • UseCase / Interactor business rules
  • Repository data transformation
  • Utility/extension functions
  • Mapper classes
  • Validation logic
  • StateFlow / SharedFlow emissions (Turbine)
02
Integration
Integration Tests
Verify how two or more components interact. Run on a real device or emulator. Slower than unit tests but catch issues that mocks can't reveal.
AndroidJUnit4 Room Testing OkHttp MockWebServer Hilt Testing WorkManager Testing
What to test
  • Room DAO queries & migrations
  • Retrofit + API response parsing
  • ContentProvider operations
  • Dependency injection wiring
  • WorkManager task chains
  • SharedPreferences / DataStore reads/writes
03
UI Testing
UI / Espresso Tests
Interact with app UI programmatically to simulate user actions. Espresso is the gold standard for View-based UIs; Compose Testing API covers Jetpack Compose screens.
Espresso Compose Testing UI Automator Barista Kakao Kaspresso
What to test
  • Button clicks and navigation
  • Form input validation feedback
  • RecyclerView item interactions
  • Dialog & BottomSheet appearance
  • Compose semantics & accessibility
  • Animation completion states
  • Orientation change persistence
04
End-to-End
End-to-End Tests
Simulate complete user journeys from app launch to final outcome, potentially crossing system boundaries like notifications, deep links, and external apps.
UI Automator Firebase Test Lab Appium Maestro BrowserStack
What to test
  • Sign-up → Login → Home flow
  • Checkout / payment user journey
  • Deep link handling from external app
  • Push notification tap → in-app navigation
  • App background → foreground state
  • Multi-process / cross-app interactions
05
Performance
Performance Tests
Measure startup time, frame rates, memory usage, battery consumption, and ANR rates to ensure your app is responsive and efficient on all devices.
Macrobenchmark Microbenchmark Baseline Profiles Perfetto Android Profiler LeakCanary
What to measure
  • Cold / warm / hot startup time (TTID / TTFD)
  • Frame rendering (Jank / slow frames)
  • Memory allocations & leaks
  • CPU usage during heavy operations
  • Scroll performance in lists
  • Battery drain over time
06
Security
Security Tests
Identify vulnerabilities before release. Android apps face unique threats from root exploitation, SSL pinning bypasses, and insecure local storage.
MobSF OWASP MASTG Frida Drozer jadx apktool
What to check
  • Insecure data storage (SharedPrefs, DB)
  • Network traffic interception / MITM
  • SSL certificate pinning bypass
  • Exported components attack surface
  • Sensitive data in logs or memory
  • Root detection & emulator detection
07
Accessibility
Accessibility Tests
Ensure your app is usable by everyone, including users with disabilities using TalkBack, Switch Access, or large text. Android's Accessibility Test Framework (ATF) runs checks automatically during Espresso tests.
ATF (AccessibilityChecks) Espresso Compose Semantics TalkBack
What to verify
  • Content descriptions on images
  • Touch target minimum size (48dp)
  • Color contrast ratios
  • Correct focus traversal order
  • Live region announcements
08
Snapshot
Screenshot / Snapshot Tests
Capture pixel-perfect screenshots of components and compare them against baselines to catch unintended visual regressions in layouts, themes, and typography.
Paparazzi Showkase Shot Roborazzi DropShots
What to capture
  • Individual Compose components
  • Light / dark theme variants
  • Loading, empty, error states
  • RTL layout rendering
  • Large font size variants
// 03 — Code

Real-World
Examples

Copy-paste ready test patterns for the most common Android testing scenarios.

LoginViewModelTest.kt
@RunWith(MockitoJUnitRunner::class)
class LoginViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Mock private lateinit var authRepository: AuthRepository

    private lateinit var viewModel: LoginViewModel

    @Before
    fun setUp() {
        viewModel = LoginViewModel(authRepository)
    }

    @Test
    fun `login with valid credentials emits Success state`() = runTest {
        // Given
        whenever(authRepository.login("user@example.com", "password123"))
            .thenReturn(Result.success(User(id = "1", email = "user@example.com")))

        // When
        viewModel.login("user@example.com", "password123")

        // Then
        assertThat(viewModel.uiState.value)
            .isInstanceOf(LoginUiState.Success::class.java)
    }

    @Test
    fun `login with empty email shows validation error`() = runTest {
        // When
        viewModel.login("", "password123")

        // Then
        val state = viewModel.uiState.value as LoginUiState.Error
        assertThat(state.message).isEqualTo("Email cannot be empty")
        verifyNoInteractions(authRepository)
    }

    @Test
    fun `login failure shows error state`() = runTest {
        // Given
        whenever(authRepository.login(any(), any()))
            .thenReturn(Result.failure(IOException("Network error")))

        // When
        viewModel.login("user@example.com", "wrong")

        // Then
        assertThat(viewModel.uiState.value)
            .isInstanceOf(LoginUiState.Error::class.java)
    }
}
UserDaoTest.kt
@RunWith(AndroidJUnit4::class)
@SmallTest
class UserDaoTest {

    private lateinit var database: AppDatabase
    private lateinit var userDao: UserDao

    @Before
    fun createDb() {
        val context = ApplicationProvider.getApplicationContext<Context>()
        database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
            .allowMainThreadQueries()
            .build()
        userDao = database.userDao()
    }

    @After
    fun closeDb() = database.close()

    @Test
    fun insertAndRetrieveUser() = runTest {
        val user = UserEntity(id = "1", name = "Alice", email = "alice@test.com")

        userDao.insert(user)

        val retrieved = userDao.getUserById("1")
        assertThat(retrieved).isEqualTo(user)
    }

    @Test
    fun updateUserEmitsNewValue() = runTest {
        val user = UserEntity(id = "2", name = "Bob", email = "bob@test.com")
        userDao.insert(user)

        userDao.update(user.copy(name = "Bobby"))

        userDao.observeUser("2").test {
            assertThat(awaitItem().name).isEqualTo("Bobby")
            cancelAndConsumeRemainingEvents()
        }
    }

    @Test
    fun deleteUserRemovesFromDb() = runTest {
        val user = UserEntity(id = "3", name = "Carol", email = "carol@test.com")
        userDao.insert(user)
        userDao.delete(user)

        assertThat(userDao.getUserById("3")).isNull()
    }
}
LoginActivityTest.kt
@RunWith(AndroidJUnit4::class)
@LargeTest
class LoginActivityTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Test
    fun successfulLogin_navigatesToHome() {
        // Type credentials
        onView(withId(R.id.emailField))
            .perform(typeText("user@test.com"), closeSoftKeyboard())

        onView(withId(R.id.passwordField))
            .perform(typeText("password123"), closeSoftKeyboard())

        // Click login
        onView(withId(R.id.loginButton))
            .perform(click())

        // Verify navigation to HomeActivity
        intended(hasComponent(HomeActivity::class.java.name))
    }

    @Test
    fun emptyEmail_showsErrorMessage() {
        onView(withId(R.id.loginButton)).perform(click())

        onView(withText("Email cannot be empty"))
            .check(matches(isDisplayed()))
    }

    @Test
    fun recyclerItem_click_opensDetail() {
        onView(withId(R.id.recyclerView))
            .perform(RecyclerViewActions
                .actionOnItemAtPosition<UserAdapter.ViewHolder>(0, click()))

        onView(withId(R.id.detailContainer))
            .check(matches(isDisplayed()))
    }
}
ProfileScreenTest.kt
class ProfileScreenTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun profileScreen_displaysUserName() {
        composeTestRule.setContent {
            ProfileScreen(user = User(name = "Alice", bio = "Android Developer"))
        }

        composeTestRule.onNodeWithText("Alice").assertIsDisplayed()
        composeTestRule.onNodeWithText("Android Developer").assertIsDisplayed()
    }

    @Test
    fun editButton_click_togglesEditMode() {
        composeTestRule.setContent {
            ProfileScreen(user = User(name = "Alice"))
        }

        composeTestRule.onNodeWithContentDescription("Edit profile").performClick()

        composeTestRule.onNodeWithTag("nameInput").assertIsDisplayed()
        composeTestRule.onNodeWithTag("nameInput").assertTextContains("Alice")
    }

    @Test
    fun loadingState_showsProgressIndicator() {
        composeTestRule.setContent {
            ProfileScreen(uiState = ProfileUiState.Loading)
        }

        composeTestRule.onNodeWithTag("loadingIndicator").assertIsDisplayed()
        composeTestRule.onNodeWithText("Alice").assertDoesNotExist()
    }

    @Test
    fun semanticsCheck_allImagesHaveContentDescription() {
        composeTestRule.setContent {
            ProfileScreen(user = User(avatarUrl = "https://..."))
        }

        composeTestRule
            .onAllNodesWithContentDescription("Profile avatar")
            .assertAll(isDisplayed())
    }
}
FeedViewModelFlowTest.kt
class FeedViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private val feedRepository = mockk<FeedRepository>()
    private lateinit var viewModel: FeedViewModel

    @Before
    fun setUp() {
        viewModel = FeedViewModel(feedRepository)
    }

    @Test
    fun `feed items flow emits loading then content`() = runTest {
        val items = listOf(FeedItem("1"), FeedItem("2"))
        coEvery { feedRepository.observeFeed() } returns flowOf(items)

        viewModel.feedItems.test {
            // Initial loading state
            assertThat(awaitItem()).isInstanceOf(FeedUiState.Loading::class.java)

            // Content state
            val content = awaitItem() as FeedUiState.Content
            assertThat(content.items).hasSize(2)

            cancelAndConsumeRemainingEvents()
        }
    }

    @Test
    fun `refresh triggers new fetch`() = runTest {
        coEvery { feedRepository.refresh() } returns Unit

        viewModel.refresh()

        coVerify(exactly = 1) { feedRepository.refresh() }
    }
}
MainActivityHiltTest.kt
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class MainActivityHiltTest {

    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val activityRule = ActivityScenarioRule(MainActivity::class.java)

    // Replace real dep with a fake
    @BindValue @JvmField
    val authRepository: AuthRepository = mockk(relaxed = true)

    @Before
    fun init() = hiltRule.inject()

    @Test
    fun loggedOutUser_seesLoginScreen() {
        every { authRepository.isLoggedIn() } returns false

        onView(withId(R.id.loginScreen))
            .check(matches(isDisplayed()))
    }

    @Test
    fun loggedInUser_seesHomeScreen() {
        every { authRepository.isLoggedIn() } returns true

        onView(withId(R.id.homeScreen))
            .check(matches(isDisplayed()))
    }
}
AppStartupBenchmark.kt
@RunWith(AndroidJUnit4::class)
class AppStartupBenchmark {

    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun startupCold() = benchmarkRule.measureRepeated(
        packageName = "com.example.myapp",
        metrics = listOf(StartupTimingMetric()),
        iterations = 5,
        startupMode = StartupMode.COLD,
    ) {
        pressHome()
        startActivityAndWait()
    }

    @Test
    fun scrollFeedJankMetrics() = benchmarkRule.measureRepeated(
        packageName = "com.example.myapp",
        metrics = listOf(FrameTimingMetric()),
        iterations = 3,
        startupMode = StartupMode.WARM,
        setupBlock = { startActivityAndWait() }
    ) {
        val device = device()
        val recycler = device.findObject(By.res("com.example.myapp:id/feedList"))
        recycler.setGestureMargin(device.displayWidth / 5)
        recycler.fling(Direction.DOWN)
    }
}
// 04 — Principles

Best Practices

Patterns that separate a solid test suite from a flimsy one.

🏗️
AAA Pattern
Structure every test with Arrange (set up state), Act (execute behavior), Assert (verify outcome). Improves readability and communicates intent clearly.
🔒
Test Isolation
Each test must be independent. No shared mutable state between tests. Use @Before / @After to set up and tear down. Tests should pass in any order.
🧩
Fake > Mock
Prefer hand-written fakes (FakeAuthRepository) over mocks for core abstractions. Fakes are more realistic, easier to reuse, and less brittle than mock setups.
MainDispatcherRule
Always replace the main dispatcher in unit tests with a TestCoroutineDispatcher via a JUnit Rule. This prevents real coroutine delays and makes tests deterministic.
📐
Test Naming
Use descriptive names like `given X when Y then Z` or `action_condition_result`. The test name should read as a specification and be understandable without reading the body.
🚫
Avoid Logic in Tests
No if/else, for loops, or complex conditionals in test bodies. Tests should be linear, predictable assertions — not programs that can themselves contain bugs.
🔄
Parametrize Tests
Use JUnit5 @ParameterizedTest or Kotest data-driven testing for multiple inputs. Avoid copy-paste tests that differ only in data.
🗃️
Test Data Builders
Create builder functions or default-argument data classes to construct test data. Keep test setup concise and focused on only what each test cares about.
📊
Coverage Goals
Aim for 80%+ line coverage on business logic (ViewModels, UseCases, Repositories). Don't chase 100% — cover meaningful branches, not generated boilerplate.
// 05 — Automation

CI/CD Pipeline

A well-structured pipeline runs your full suite on every commit, catching regressions before they reach production.

🧹
Lint & Static
ktlint, detekt, lint checks. Fail fast on style issues.
Unit Tests
JVM tests. Fast feedback. Must pass before proceeding.
🏗️
Build APK
Assemble debug and test APKs. Upload as artifacts.
📱
Instrumented
Run on emulator via Firebase Test Lab or GHA emulator.
📸
Snapshot
Paparazzi / Roborazzi compare against baseline PNGs.
🚀
Deploy
Promote to internal track on green. Auto-deploy on merge.
// 06 — Advanced

Advanced Topics

Patterns for large, production-scale Android codebases.

🌊 Testing Coroutines & Flows
Coroutines and Flows require special handling. Use Turbine for Flow assertions and runTest for coroutine scopes.
  • Use runTest to wrap coroutine test bodies
  • Turbine's .test { } for Flow emissions
  • advanceTimeBy() for delay-based logic
  • TestScope.backgroundScope for long-lived flows
  • UnconfinedTestDispatcher for immediate execution
📡 Network Testing
Never hit real APIs in tests. Use MockWebServer to intercept HTTP calls and return fixture responses for reliable, fast, offline tests.
  • OkHttp MockWebServer enqueues responses
  • Load JSON fixtures from test resources
  • Test error codes (4xx, 5xx) and timeouts
  • Verify request body and headers
  • Use WireMock for complex API scenarios
🔀 Testing Navigation
Test Navigation Component routes and arguments without launching full Activities using the NavController test APIs.
  • TestNavHostController for unit-level nav tests
  • findNavController in Espresso fragment tests
  • Verify currentDestination after actions
  • Test deep link argument parsing
  • Compose: NavHost with test controller
💉 Hilt Test Strategies
Hilt's test APIs allow you to replace production modules with test doubles at the injection site, without modifying production code.
  • @TestInstallIn replaces entire modules
  • @BindValue replaces individual bindings
  • @UninstallModules removes a module entirely
  • launchFragmentInHiltContainer utility
  • Use HiltAndroidRule before ActivityScenarioRule
📱 Firebase Test Lab
Run your instrumented tests on a real device matrix in the cloud — hundreds of real device and OS combinations, with screenshots and video on failure.
  • gcloud firebase test android run
  • Robo tests require zero test code to write
  • Instrumentation tests on physical devices
  • Game Loop testing for game apps
  • Integrate via Fastlane + GitHub Actions
// 07 — Reference

Tools Cheatsheet

The definitive table of every major Android testing tool and when to use it.

Tool Category Runs On Primary Use
JUnit4 / JUnit5UnitJVMBase test runner, assertions, test rules for all local tests
Mockito / MockKUnitJVMMock/stub dependencies; MockK is idiomatic Kotlin
TruthUnitJVMFluent assertions that produce readable failure messages
TurbineUnitJVMTest Kotlin Flows — await emissions, check errors
KotestUnitJVMProperty-based, behavior-driven test framework for Kotlin
AndroidJUnit4IntegrationDeviceInstrumented test runner for on-device tests
Room In-MemoryIntegrationDeviceTest DAO queries with an in-memory Room database
MockWebServerIntegrationJVM/DeviceIntercept HTTP calls and return fixture JSON responses
WorkManager TestIntegrationDeviceTest Worker logic and chaining with TestListenableWorkerBuilder
EspressoUIDeviceSynchronised View UI testing — clicks, scrolls, checks
Compose TestingUIDevice / JVMSemantics-based testing for Jetpack Compose screens
BaristaUIDeviceEspresso wrapper with more readable API and flakiness handling
KaspressoUIDeviceKotlin DSL over Espresso + UI Automator with built-in flakiness handling
UI AutomatorE2EDeviceCross-app interactions, system UI, notifications, settings
MaestroE2EDeviceYAML-based E2E flows, no code required, fast iteration
AppiumE2EDeviceCross-platform E2E testing (Android + iOS from same test)
MacrobenchmarkPerfDeviceMeasure startup time, scrolling, and transitions at macro level
MicrobenchmarkPerfDeviceBenchmarks for individual functions — allocation, compute speed
LeakCanaryPerfDeviceAutomatic memory leak detection in debug builds
PaparazziSnapshotJVMScreenshot tests for Compose/Views without a device — fast CI
RoborazziSnapshotJVMRobolectric-based screenshot tests with rich diff output
Firebase Test LabE2E/CloudCloud DevicesRun tests on real device matrix in Google Cloud — CI integration